﻿using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Caliburn.Micro;
using SS.WPF.Framework.Markup;
using Control = System.Windows.Controls.Control;

namespace SS.WPF.Toolkit.PropertyEditor
{
    using System;
    using System.Collections.Generic;

    /// <summary>
    /// The class is used to manage properties
    /// </summary>
    [TemplatePart(Name = "PART_Filter", Type = typeof(TextBox))]
    [TemplatePart(Name = "PART_Items", Type = typeof(DataGrid))]
    public class PropertyEditor : Control
    {
        #region Constants and Fields

        private readonly IChecker _dirtyChecker;
        private readonly IDictionary<string, object> _originalData = new Dictionary<string, object>();
        private readonly IDictionary<string, string> _currentData = new Dictionary<string, string>();
        private readonly IDictionary<string, bool> _currentDirty = new Dictionary<string, bool>();
        public readonly object NullInstance = new object();
        private readonly IList<string> _errorFields = new List<string>();
        int _loadingRow = 0;
        private DataGrid _datagrid;

        #region Filtered

        /// <summary>
        /// The preview dirty check event.
        /// </summary>
        public static readonly RoutedEvent FilteredEvent = EventManager.RegisterRoutedEvent(
            "Filtered", RoutingStrategy.Bubble, typeof(EventHandler<RoutedEventArgs>), typeof(PropertyEditor));

        public event EventHandler<RoutedEventArgs> Filtered
        {
            add
            {
                this.AddHandler(FilteredEvent, value);
            }
            remove
            {
                this.RemoveHandler(FilteredEvent, value);
            }
        }

        #endregion

        #region Filter

        /// <summary>
        /// The property is bound with textbox for filtering Items
        /// <para>
        /// If this filter is set, only properties Containing this Text will be displayed
        /// </para>
        /// </summary>
        public static DependencyProperty FilterProperty = DependencyProperty.Register(
            "Filter", typeof(string), typeof(PropertyEditor), new FrameworkPropertyMetadata("", OnFilterChanged));

        public string Filter
        {
            get
            {
                return (string)this.GetValue(FilterProperty);
            }
            set
            {
                this.SetValue(FilterProperty, value);
            }
        }

        /// <summary>
        /// The method is invoked when Filter property changed
        /// </summary>
        /// <param name="d">Refer to owner of control bound with Filter property</param>
        /// <param name="e">Event arguments</param>
        private static void OnFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var propertyGrid = d as PropertyEditor;

            if (propertyGrid != null)
            {
                foreach (var property in propertyGrid.Properties.OfType<Property>())
                {
                    propertyGrid._currentDirty[property.Description] = property.IsDirty;
                }

                propertyGrid.Refresh();
                propertyGrid.SetOriginalData();
                propertyGrid.DetectError();
                DirtyChecker.RaiseRecheckDirtyEvent(propertyGrid);
            }
        }

        /// <summary>
        /// Keep orginal data
        /// <para>
        /// The original data is used when user want to reset any data changed to orginal
        /// </para>
        /// </summary>
        private void SetOriginalData()
        {
            foreach (var item in this.Properties)
            {
                var property = item as Property;
                if (property != null)
                {
                    property.OriginalData = this._originalData[property.Description];
                }
            }
        }

        public void DetectError()
        {
            var hasError = this._errorFields.Count > 0;

            this.HasError = hasError;
        }

        #endregion

        #region HasError

        public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached(
                "HasError", typeof(bool), typeof(PropertyEditor), new PropertyMetadata(false, OnHasErrorChanged));

        public bool HasError
        {
            get
            {
                return (bool)this.GetValue(HasErrorProperty);
            }
            set
            {
                this.SetValue(HasErrorProperty, value);
            }
        }

        private static void OnHasErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {

        }

        #endregion

        #region PropertySource

        /// <summary>
        /// Refers to Propery Source of Consumer that using PropertyEditor
        /// </summary>
        public static readonly DependencyProperty PropertySourceProperty = DependencyProperty.Register(
            "PropertySource", typeof(object), typeof(PropertyEditor),
            new FrameworkPropertyMetadata(null, OnPropertySourceChanged, CoercePropertySourceCallback));

        /// <summary>
        /// Get set instance of ProperySource 
        /// that is refered to Propery Source of Consumer that using PropertyEditor
        /// </summary>
        public object PropertySource
        {
            get
            {
                return this.GetValue(PropertySourceProperty);
            }
            set
            {
                this.SetValue(PropertySourceProperty, value);
            }
        }

        /// <summary>
        /// The method is invoked when PropertySource property changed
        /// </summary>
        /// <param name="d">Refer to owner of control bound with PropertySource property</param>
        /// <param name="e">Event arguments</param>
        private static void OnPropertySourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var propertyGrid = d as PropertyEditor;

            if (propertyGrid != null)
            {
                propertyGrid._originalData.Clear();
                propertyGrid._currentDirty.Clear();
                propertyGrid.Refresh();

                foreach (var item in propertyGrid.Properties)
                {
                    var property = item as Property;
                    if (property != null)
                    {
                        property.OriginalData = property.Value;
                        if (property.OriginalData != null)
                        {
                            propertyGrid._originalData[property.Description] = property.OriginalData;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// The callback method, it is invoked when the PropertySource has been changed
        /// </summary>
        /// <param name="d">Refer to owner of control bound with Filter property</param>
        /// <param name="value">The value has been changed</param>
        /// <returns></returns>
        private static object CoercePropertySourceCallback(DependencyObject d, object value)
        {
            var propertyGrid = (PropertyEditor)d;
            if (value == null)
            {
                return propertyGrid.NullInstance;
            }

            return value;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Refer to collection of Property, 
        /// this collection is built from Property Source of Consumer
        /// </summary>
        public static readonly DependencyProperty PropertiesProperty = DependencyProperty.Register(
            "Properties", typeof(BindableCollection<Item>), typeof(PropertyEditor),
            new FrameworkPropertyMetadata(new BindableCollection<Item>(), OnPropertiesChanged));

        /// <summary>
        /// Get or set instance of Properties that is refered to collection of Property, 
        /// this collection is built from Property Source of Consumer
        /// </summary>
        public BindableCollection<Item> Properties
        {
            get
            {
                return (BindableCollection<Item>)this.GetValue(PropertiesProperty);
            }
            set
            {
                this.SetValue(PropertiesProperty, value);
            }
        }

        /// <summary>
        /// The method is invoked when Properties property changed
        /// </summary>
        /// <param name="d">Refer to owner of control bound with PropertiesProperty</param>
        /// <param name="e">Event arguments</param>
        private static void OnPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var properties = e.OldValue as BindableCollection<Item>;
            if (properties != null)
            {
                foreach (Item item in properties)
                {
                   item.Dispose();
                }
            }
        }

        #endregion

        #region ValidationRule

        public static readonly DependencyProperty ValidationRuleProperty =
            DependencyProperty.RegisterAttached(
                "ValidationRule", typeof(System.Windows.Controls.ValidationRule), typeof(PropertyEditor), new PropertyMetadata(OnValidationRuleChanged));

        public static System.Windows.Controls.ValidationRule GetValidationRule(DependencyObject control)
        {
            return (System.Windows.Controls.ValidationRule)control.GetValue(ValidationRuleProperty);
        }

        public static void SetValidationRule(DependencyObject control, System.Windows.Controls.ValidationRule value)
        {
            control.SetValue(ValidationRuleProperty, value);
        }

        private static void OnValidationRuleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {

        }

        #endregion

        #endregion

        #region Ctors

        static PropertyEditor()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(PropertyEditor),
                                                     new FrameworkPropertyMetadata(typeof(PropertyEditor)));
        }

        public PropertyEditor()
        {
            this._dirtyChecker = new PropertyGridDirtyChecker(this);
            DirtyChecker.SetChecker(this, this._dirtyChecker);
            this.AddHandler(Selector.SelectionChangedEvent, new SelectionChangedEventHandler(OnSelectionChanged));
            this.AddHandler(TextBoxBase.TextChangedEvent, new TextChangedEventHandler(OnTextChanged));
            this.AddHandler(UIElement.GotFocusEvent, new RoutedEventHandler(this.OnControlGotFocus));
            this.AddHandler(PreviewKeyDownEvent, new KeyEventHandler(this.OnControlKeyDown));
            this.IsEnabledChanged += new DependencyPropertyChangedEventHandler(PropertyEditor_IsEnabledChanged);
            FocusManager.SetIsFocusScope(this, true);            
        }

        #endregion

        #region Override FrameworkElement

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            this._datagrid = this.Template.FindName("PART_Items", this) as DataGrid;
            if (this._datagrid != null)
            {
                this._datagrid.LoadingRow += new EventHandler<DataGridRowEventArgs>(dg_LoadingRow);
                ((INotifyCollectionChanged)this._datagrid.Items.SortDescriptions).CollectionChanged += new NotifyCollectionChangedEventHandler(PropertyEditor_CollectionChanged);
            }

            Refresh();
        }

        void PropertyEditor_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
                      new Action(delegate
                      {
                          this.RaiseEvent(new RoutedEventArgs(FilteredEvent));
                      }));
        }

        void dg_LoadingRow(object sender, DataGridRowEventArgs e)
        {
            _loadingRow++;
            if (_loadingRow == this.Properties.Count)
            {
                this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
                       new Action(delegate
                       {
                           this.RaiseEvent(new RoutedEventArgs(FilteredEvent));
                       }));
            }
        }
        
        #endregion

        #region Help methods

        /// <summary>
        /// This method is executed for filtering items in PropertyEditor
        /// </summary>
        public void Refresh()
        {
            _loadingRow = 0;
            if (this.PropertySource == null)
            {
                this.Properties = new BindableCollection<Item>();
            }
            else
            {
                // Binding ItemSource of control to Properties
                var properties = new PropertyCollection(this.PropertySource, this.Filter.Trim().ToLower());
                this.Properties = properties.Items;
            }
        }

        private static void OnTextChanged(object sender, TextChangedEventArgs e)
        {
            var textBoxBase = ((TextBox)e.OriginalSource);

            var des = Framework.Markup.Control.GetDescription(textBoxBase);
            var propertyEditor = ((PropertyEditor)sender);
            if (des != null)
            {
                if (string.IsNullOrEmpty(textBoxBase.Text))
                {
                    propertyEditor._currentData[des] = textBoxBase.Text;
                }
                else
                {
                    propertyEditor._currentData.Remove(des);
                }
            }

            DetectError(sender, propertyEditor);
            DirtyChecker.RaiseRecheckDirtyEvent((FrameworkElement)sender);
        }

        private static void DetectError(object originalSource, PropertyEditor propertyEditor)
        {
            var frameworkElement = (FrameworkElement)originalSource;

            var des = Framework.Markup.Control.GetDescription(frameworkElement);
            if (System.Windows.Controls.Validation.GetHasError(frameworkElement))
            {
                if (!propertyEditor._errorFields.Contains(des))
                {
                    propertyEditor._errorFields.Add(des);
                }
            }
            else
            {
                propertyEditor._errorFields.Remove(des);
            }

            var hasError = propertyEditor._errorFields.Count > 0;
            if (propertyEditor.HasError != hasError)
            {
                propertyEditor.HasError = hasError;
            }
        }

        private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.OriginalSource is ComboBox)
            {
                DirtyChecker.RaiseRecheckDirtyEvent((FrameworkElement)sender);
            }
        }

        private void OnControlGotFocus(object sender, RoutedEventArgs e)
        {
            var cell = e.OriginalSource as DataGridCell;
            if (cell != null)
            {
                if (cell.Column.DisplayIndex == 1)
                {
                    cell.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
                }
            }
        }

        private void OnControlKeyDown(object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Down:
                    {
                        var focused = FocusManager.GetFocusedElement(this) as FrameworkElement;
                        if (focused != null)
                        {
                            if ((focused is ComboBoxItem))
                            { }
                            else if (!(focused is ComboBox))
                            {

                                focused.MoveFocus(new TraversalRequest(FocusNavigationDirection.Down));
                                e.Handled = true;
                                if (_datagrid.SelectedIndex == -1)
                                {
                                    _datagrid.SelectedIndex = 0;
                                }
                                else if (_datagrid.SelectedIndex < _loadingRow)
                                {
                                    _datagrid.SelectedIndex = _datagrid.SelectedIndex + 1;
                                }

                            }
                            else
                            {
                                var cbo = (ComboBox)focused;
                                if (cbo.SelectedIndex == cbo.Items.Count - 1)
                                {
                                    focused.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
                                    e.Handled = true;
                                    if (_datagrid.SelectedIndex < _loadingRow)
                                    {
                                        _datagrid.SelectedIndex = _datagrid.SelectedIndex + 1;
                                    }
                                }
                            }
                        }
                    }
                    break;
                case Key.Up:
                    {
                        var focused = FocusManager.GetFocusedElement(this) as FrameworkElement;
                        if (focused != null)
                        {
                            if ((focused is ComboBoxItem))
                            { }
                            else if (!(focused is ComboBox))
                            {
                                focused.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
                                e.Handled = true;

                                if (_datagrid.SelectedIndex == 0)
                                {
                                    _datagrid.SelectedIndex = -1;
                                }
                                else if (_datagrid.SelectedIndex > 0)
                                {
                                    _datagrid.SelectedIndex = _datagrid.SelectedIndex - 1;
                                }
                            }
                            else
                            {
                                var cbo = (ComboBox)focused;

                                if (cbo.SelectedIndex == 0)
                                {
                                    focused.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
                                    e.Handled = true;
                                    if (_datagrid.SelectedIndex > 0)
                                    {
                                        _datagrid.SelectedIndex = _datagrid.SelectedIndex - 1;
                                    }
                                }
                            }
                        }

                    }
                    break;
            }
        }

        private void PropertyEditor_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if ((bool)e.NewValue == false)
            {
                var propertyGrid = sender as PropertyEditor;
                if (propertyGrid != null)
                {
                    propertyGrid.Filter = string.Empty;
                }
            }
        }

        #endregion

        #region PropertyGridDirtyChecker

        private class PropertyGridDirtyChecker : IChecker
        {
            private readonly PropertyEditor _editor;

            public PropertyGridDirtyChecker(PropertyEditor editor)
            {
                _editor = editor;
            }

            #region Implementation of IChecker

            public bool CheckDirty(object target, object value)
            {
                bool isdirty = false;

                foreach (var item in this._editor.Properties)
                {
                    var property = item as Property;
                    if (property == null)
                    {
                        continue;
                    }

                    object original;

                    var description = property.Description;
                    if (this._editor._originalData.TryGetValue(description, out original))
                    {
                        var curdirty = (!original.Equals(property.Value));

                        string guiValue;
                        if (this._editor._currentData.TryGetValue(description, out guiValue))
                        {
                            curdirty = curdirty || !guiValue.Equals(property.Value.ToString());
                        }


                        property.IsDirty = curdirty;
                        if (curdirty)
                        {
                            isdirty = true;
                        }

                        this._editor._currentDirty[description] = property.IsDirty;
                    }
                }

                if (isdirty)
                {
                    return true;
                }

                var any = this._editor._currentDirty.Any(x => x.Value);
                if (any)
                {
                    return true;
                }

                return isdirty;
            }

            public object Original
            {
                get
                {
                    return this._editor._originalData.Values.ToList();
                }
            }

            public bool Handled
            {
                get
                {
                    return true;
                }
            }

            /// <summary>
            /// Reset to original
            /// </summary>
            /// <returns>True to continued checking</returns>
            public bool Reset()
            {
                var count = VisualTreeHelper.GetChildrenCount(this._editor);
                for (int i = 0; i < count; i++)
                {
                    var child = VisualTreeHelper.GetChild(this._editor, i);
                    DirtyChecker.ReverseOriginalData(child);
                }

                var pros = new PropertyCollection(this._editor.PropertySource, string.Empty);
                foreach (var item in pros.Items)
                {
                    var property = item as Property;
                    if (property == null)
                    {
                        continue;
                    }

                    object value;
                    if (this._editor._originalData.TryGetValue(property.Description, out value))
                    {
                        property.Value = value;
                    }
                }

                foreach (var item in this._editor._currentDirty.Keys.ToList())
                {
                    this._editor._currentDirty[item] = false;
                }

                this._editor.Refresh();
                this._editor.SetOriginalData();
                this._editor.DetectError();
                return false;
            }

            #endregion
        }

        #endregion
    }
}
